package org.apache.ibatis.binding;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.ibatis.binding.MapperMethod.MethodSignature;
import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.util.MapUtil;

import com.sneakxy.mybatis.commons.repository.query.parser.MybatisCommonsParamNameResolver;

/**
 * @author 潜行的青衣
 */
public class MybatisCommonsMapperProxy<T> extends MapperProxy<T> {

	/**
	 * 
	 */
	private static final long serialVersionUID = -2465620582991346862L;

	/**
	 * @param sqlSession
	 * @param mapperInterface
	 * @param methodCache
	 */
	public MybatisCommonsMapperProxy(SqlSession sqlSession, Class<T> mapperInterface,
			Map<Method, MapperMethodInvoker> methodCache) {
		super(sqlSession, mapperInterface, methodCache);
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		try {
			if (Object.class.equals(method.getDeclaringClass())) {
				return method.invoke(this, args);
			} else {
				SqlSession sqlSession = (SqlSession) FieldUtils.readField(this, "sqlSession", true);
				return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
			}
		} catch (Throwable t) {
			throw ExceptionUtil.unwrapThrowable(t);
		}
	}

	@SuppressWarnings({ "unchecked", "resource" })
	private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
		try {
			Map<Method, MapperMethodInvoker> methodCache = (Map<Method, MapperMethodInvoker>) FieldUtils.readField(this,
					"methodCache", true);
			return MapUtil.computeIfAbsent(methodCache, method, m -> {
				if (m.isDefault()) {
					try {
						Object privateLookupInMethod = FieldUtils.readStaticField(MapperProxy.class,
								"privateLookupInMethod", true);
						if (privateLookupInMethod == null) {
							MethodHandle methodHandler = (MethodHandle) MethodUtils.invokeMethod(this,
									"getMethodHandleJava8", method);
							return new DefaultMethodInvoker(methodHandler);
						} else {
							MethodHandle methodHandler = (MethodHandle) MethodUtils.invokeMethod(this,
									"getMethodHandleJava9", method);
							return new DefaultMethodInvoker(methodHandler);
						}
					} catch (IllegalAccessException | InvocationTargetException
							| NoSuchMethodException e) {
						throw new RuntimeException(e);
					}
				} else {
					try {
						SqlSession sqlSession = (SqlSession) FieldUtils.readField(this, "sqlSession", true);
						Class<T> mapperInterface = (Class<T>) FieldUtils.readField(this, "mapperInterface", true);
						
						MapperMethod mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
						MethodSignature methodSignature = (MethodSignature) FieldUtils.readField(mapperMethod, "method", true);
						
						MybatisCommonsParamNameResolver resolver = new MybatisCommonsParamNameResolver(sqlSession.getConfiguration(), method);
						FieldUtils.writeField(methodSignature, "paramNameResolver", resolver, true);
						return new PlainMethodInvoker(mapperMethod);
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					}
					return null;
				}
			});
		} catch (RuntimeException re) {
			Throwable cause = re.getCause();
			throw cause == null ? re : cause;
		}
	}

	private static class PlainMethodInvoker implements MapperMethodInvoker {
		private final MapperMethod mapperMethod;

		public PlainMethodInvoker(MapperMethod mapperMethod) {
			super();
			this.mapperMethod = mapperMethod;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
			return mapperMethod.execute(sqlSession, args);
		}
	}

	private static class DefaultMethodInvoker implements MapperMethodInvoker {
		private final MethodHandle methodHandle;

		public DefaultMethodInvoker(MethodHandle methodHandle) {
			super();
			this.methodHandle = methodHandle;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
			return methodHandle.bindTo(proxy).invokeWithArguments(args);
		}
	}

}
